<?php
namespace DRY\Module;
/**
 * DonReY Framework 2012 :: User authentication system
 * Supports various credential providers ( database, csv file, ?, ... )
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/authentication.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Authentication
 */
class Authentication extends base
{
	const
		Autoload_Directory = 'authentication',
		Autoload_Prefix = 'DRY\Authentication\Storage',
		Autoload_Suffix = 'storage'
		;

	/**
	 * Credentials storage object. All storage objects must respond in the same way indifferent of the method they use to store credentials!
	 * @var \DRY\Aythentication\Storage
	 */
	private $cStorage;

	/**
	 * Active user session - Contains username, time of successful credentials check (start), extra data stored in credentials (as it was at check time), extra data passed form the check_Auth method
	 * Stored in a persistence container (normally Session Container)
	 * @var DRY_Persistence_Container
	 */
	private $session = null;

	/**
	 * Module Constructor
	 * @param string $alias
	 * @param SimpleXMLElement $config
	 */
	public function __construct($alias, $config)
	{
		parent::__construct($alias, $config);

		// Authentication module uses Observers pattern to notify other code of it's actions ( for example an user logs in/out or registers )
		\DRY\core::register_Subject($this->_alias, $this);

		// TODO: Support cookie based auto-login (actually, another Persistence Container)

		// Session
		$this-> session = $this[(string)$this-> CFG-> session-> storage]-> factory_Container((string)$this-> CFG-> session-> container, (string)$this-> CFG-> session-> container['type']?:'session');
		// Reads the active session if it exists ( this is only a preload, the persistence engine works even without this line )
		$this-> session-> read_All();

		// initialization of credentials handler/storage object
		$class_name=self::Autoload_Prefix.'_'.$this-> CFG-> storage['type'];
		$this-> cStorage=new $class_name($this-> CFG-> storage, $this);

		// only gets executed if there's an active session
		if(count($this-> session)>0) {
			if(isset($this-> CFG-> session-> timeout)) {
				if($this-> CFG-> session-> timeout['autoreset']=='true')
					$this-> session-> timeout((int)$this-> CFG-> session-> timeout);
			}
		}

	}

	/**
	 * Creates an account using the storage engine
	 * @param string $account Account identifier
	 * @param string $password Password or password hash
	 * @param mixed $status Initial status of the account 0 = disabled, 1 = enabled: can "Log in"
	 * @param mixed $extra Other information which will be stored alongside the account
	 */
	public function register_Account($account, $password, $status=0, $extra=array())
	{
		// TODO: Regex to allow only alphanum, dash, underscore, dot and a few other non-dangerous characters in account name

		$result = $this-> cStorage-> create_Credentials($account, $password, $status, $extra);
		call_user_func($this->_notify, $this->_alias, "register_Account", array(
			'account'=> $account,
			'status'=> $status,
			'result'=> $result
		));
		return $result;
	}

	/**
	 * Verifies an account's credentials and returns it's stored status
	 * @param string $account
	 * @param string $password
	 */
	public function check_Credentials($account, $password)
	{
		$result = $this-> cStorage-> check_Credentials($account, $password);
		call_user_func($this->_notify, $this->_alias, "check_Credentials", array(
			'account'=> $account,
			'result'=> $result
		));
		return $result;
	}

	/**
	 * Verifies credentials then creates the session if authentication successful
	 * @param string $account
	 * @param string $password
	 * @param mixed $session_extra This data is added to the active session
	 */
	public function authenticate_Account($account, $password, $session_extra=null)
	{
		// if already authenticated do not attempt to authenticate again
		if(count($this-> session)>0)
			return true;

		$result = $this-> check_Credentials($account, $password);
		if(is_null($result) || $result!=true)
			return $result; // failed

		$this-> create_Session($account, $result, $session_extra);
		return true;
	}

	/**
	 * Logs the current user off
	 */
	public function deauthenticate()
	{
		// TODO: Support recording of deauthenticate time (example: "Last time logged" variable)
		$this-> destroy_Session();
	}

	/**
	 * Uses a Persistence container to record information about the user ( usually PHP Session but it can be any type of container )
	 * @param string $account
	 * @param array $auth_result
	 * @param mixed $session_extra
	 */
	protected function create_Session($account, $auth_result, $session_extra=null)
	{
		if(isset($this-> CFG-> session-> timeout)) {
			$this-> session-> timeout((int)$this-> CFG-> session-> timeout);
		}
		$this-> session-> account = $account;
		$this-> session-> start = time();
		$this-> session-> extra = $session_extra;
		$this-> session-> credentials_extra = $this-> cStorage-> read_Extra();

		call_user_func($this->_notify, $this->_alias, "create_Session", array(
			'account'=> $account,
			'session_start'=> time()
		));
	}

	/**
	 * Cancels the active session (effectively deauthenticating the user)
	 */
	protected function destroy_Session()
	{
		call_user_func($this->_notify, $this->_alias, "destroy_Session", array(
			'account'=> $this-> session-> account,
			'session_start'=> $this-> session-> start
		));
		$this-> session-> destroy();
	}

	protected function update_Session($credentials_extra = null, $session_extra = null)
	{
		if(!is_null($credentials_extra))
			$this-> session-> credentials_extra = array_merge($this-> session-> credentials_extra, $credentials_extra);
		if(!is_null($session_extra))
			$this-> session-> extra = $session_extra;
	}

	/**
	 * Reads a session variable
	 *
	 * @param string $var
	 * @return mixed
	 */
	public function __get($var)
	{
		if(isset($this-> session-> $var))
			return $this-> session-> $var;
		else
			return null;
	}

	/**
	 * Function pass-through to credentials storage driver
	 * @param string $fname
	 * @param array $args
	 * @return mixed return from the storage driver
	 */
	public function __call($fname,$args)
	{
		if($fname == 'update_Extra' && is_null($args[1])) {
			$this-> update_Session($args[0]);
		}

		// TODO: A way to check if the function actually exists
		return call_user_func_array(array($this-> cStorage, $fname),$args);
	}

}

namespace DRY\Authentication;

/**
 * Mandatory functions for Credentials Storage system, used by the Authentication module. Any type of storage engine must implement those functions
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/authentication.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Authentication
 */
interface Storage
{
	/**
	 * Sets how the password is stored and passed
	 * - null, 0, false = store exactly as sent
	 * - not 0, true = store encrypted/hashed with the built-in encryption/hashed engine (if it exists)
	 * - callback = use the callback function to transform the password before storing/comparing it. This is ment to replace the built-in encryption/hashing engine
	 *   The callback function will receive the following parameters:
	 *     - password ( as sent by functions )
	 *     - username ( as sent by functions )
	 *     - configuration of storage engine, which could contain anything (it's up to the function to use the data as it deems fit)
	 * @param mixed $mixed
	 */
	public function set_Password_Encryption($mixed);

	/**
	 * Create an account with a password
	 * @param string $account
	 * @param string $password password in clear, encryption/hashing is handled by the storage engine, if necessary
	 * @param mixed $state State The state of the account. The default value, "0" means the account is ready and functional
	 * @param mixed $extra Other information that can be passed to the storage engine (can by anything)
	 */
	public function create_Credentials($account, $password, $status=0, $extra=null); // create an account with a password

	/**
	 * Checks credentials against the stored values
	 * (not "Read" because credentials are not supposed to be read, they are just compared/checked)
	 * @param string $account
	 * @param string $password
	 * @return mixed Account state, "null" if the account does not exist, "false" if the password check failed for the passed account name, or {status} value if it succeeds
	 */
	public function check_Credentials($account, $password);

	/**
	 * Update state and/or password for a stored account
	 * @param string $account
	 * @param mixed $status New status for this account (for example to enable/disable the account)
	 * @param string $password Optional - also change password
	 */
	public function update_Credentials($account=null, $status=null, $password=null);

	/**
	 * Removes the account from the storage (it also removes extra data)
	 * @param string $account
	 */
	public function delete_Credentials($account=null);

	/**
	 * Reads the stored extra data for an account, or for the one that passed the check method
	 * @param string $account
	 */
	public function read_Extra($account=null);

	/**
	 * Updates the attached extra data for an account, or for the one that passed the check method
	 * This method can also remove the extra, if called without any data in the "extra" argument
	 * @param string $account
	 * @param mixed $extra
	 */
	public function update_Extra($extra=null, $account=null);
}
